# forward-compat boilerplate
from __future__ import absolute_import
from __future__ import with_statement
__metaclass__ = type

import urlparse
import functools

from fenton import util
from fenton import getmeta
from fenton import widgets
from fenton import logging




class MethodWrapper:
    __func = property(lambda x:getattr(x.__self, x.__name))
    def __init__(self, m):
        f = m.im_func
        if getattr(m.im_class, f.__name__).im_func is not f:
            raise TypeError('method %s is not known by that name (decorated?)' % f.__name__)
        self.__self = m.im_self
        self._name = self.__name = f.__name__
        functools.wraps(f)(self)

    def __call__(self, *args, **kw):
        return self.__func(*args, **kw)

    def __repr__(self):
        return '<%s.%s>' % (self.__self,
                            self.__name)




class Screen:

    form_error_msg = 'Form has errors (click icon for detail)'
    namespace = 'Fenton'

    request = property(lambda x:x.context.request)
    app     = property(lambda x:x.request.app)
    debug   = property(lambda x:x.request.app.debug)
    meta    = property(lambda x:getmeta(x.model))
    has_error = property(lambda x:any(x.errors.values()))

    parent  = None
    screen_title   = None
    template_file = None
    screen_message = None
    model = None
    referer = None
    bind_update = None

    registerable = False
    heartbeat = True
    editable = False
    modal = False

    def __init__(self, context, model, priv=None,
                 parent=None, key=None, index=None):
        self.context = context
        self.priv = priv
        self.parent = parent
        self.model = model
        self.errors = {}
        self.bindings = {}
        self.child_key = key
        self.child_index = index


    def __repr__(self):
        n = self.__class__.__name__
        m = type(self.model).__name__
        return '<%s(%s)>' % (n, m)

    __current_id = 0
    def __seq(self):
        self.__current_id += 1
        return self.__current_id

    def next_id(self):
        return 'x0' + util.to_base62(self.__seq())

    def bind(self, func, *args, **kw):
        if hasattr(func, 'im_self'):
            func = MethodWrapper(func)
        return self._bind(func, *args, **kw)

    def bind_logger(self):
        return self.bind(self.logmsg)

    def logmsg(self):
        msg = self.request.params.get('msg')
        logging.log.info('[JS] %s', msg)

    def bind_heartbeat(self):
        if self.heartbeat:
            return self.bind(self.heartbeat_ping)

    def heartbeat_ping(self):
        pass

    def get_form(self):
        from fenton import form
        return form.Form(self.context, self.model, self.editable, self.errors)

    def get_priv(self, name):
        pass

    def check(self, priv='view'):
        if isinstance(priv, basestring):
            priv = self.get_priv(priv)
        from fenton import security
        return security.check(priv, self.context, self.model)

    def _bind(self, f, *args, **kw):
        key = self.next_id()
        self.bindings[key] = (f, args, kw)
        return key

    def get_binding(self, key):
        if key not in self.bindings:
            return None
        return self.bindings.get(key)

    def get_parent(self):
        return self.parent

    def get_path(self):
        if not self.registerable:
            return None
        if self.model is None:
            return None
        return getmeta(self.model).get_object_path(self.context, self.model)

    def get_link(self, parent):
        return widgets.link(parent, url=self.get_url(), label=self.get_title())

    def get_url(self):
        path = self.get_path()
        if not path:
            return None
        if path == '/': path = ''
        url = self.request.app_url(path)
        args = self.get_args()
        if args:
            from fenton import form
            args = form.encode({self.request.model_key: args})
            qs = '&'.join('%s=%s'%pair for pair in args.items() if pair[1])
            scheme, host, path, _, frag = urlparse.urlsplit(url)
            url = urlparse.urlunsplit((scheme, host, path, qs, frag))
        return url

    def add_callback(self, name):
        self.vars.setdefault('run_callbacks', []).append(name)

    def callback_expr(self, expr):
        self.vars['callback_expr'] = expr

    def refresh(self):
        self.request.refresh(self)

    def replace(self, screen):
        self.context.screen = screen
        screen.refresh()

    def pop(self):
        p = self.get_parent()
        assert p
        self.replace(p)

    def thaw(self):
        if self.model is not None:
            getmeta(self.model).thaw_model(self.context, self.model)
        if self.parent:
            self.parent.thaw()

    def render(self, widget=None):
        if widget is None:
            widget = self.get_widget()
        p = widgets._collector(self)
        widget.set_parent(p)
        p.compile()
        self.vars['widgets'] = p.widgets
        return self.request.render(widget)

    def update(self, args=None):
        pass

    def get_title(self):
        return self.screen_title

    def set_message(self, type, msg):
        self.screen_message = (type, msg)

    def get_message(self):
        msg = self.screen_message
        if not msg and self.has_error:
            msg = ('error', self.form_error_msg)
        self.screen_message = None
        return msg

    def get_args(self):
        return self.get_form().get_args()

    @util.lazyattr
    def vars(self):
        return self.get_vars()

    def get_vars(self):
        return {}

    def get_widget(self):
        self.vars['message'] = self.get_message()
        frame = widgets.screen_frame(self, title=self.get_title())
        self.build_header(frame)
        self.build_actions(frame)
        self.build_body(frame)
        return frame

    def build_header(self, frame):
        widgets.header_panel(frame, title=self.get_title())

    def build_actions(self, frame):
        panel = widgets.left_actions(frame)
        self.left_actions(panel)

    def build_body(self, frame):
        body = widgets.right_body(frame)
        self.right_body(body)

    def left_actions(self, panel):
        pass

    def right_body(self, body):
        pass




class Plain(Screen):
    registerable = True
    def render(self):
        self.request.response.content_type = self.model.content_type
        return str(self.model)




class CustomScreen(Screen):
    registerable = True
    def build_body(self, frame):
        widgets.screen_widget(frame, template_file=self.template_file)




class Dialog(Screen):
    registerable = True
    def build_body(self, frame):
        sheet = widgets.sheet_body(frame)
        self.build_dialog(sheet)
        self.build_dialog_buttons(widgets.button_bar(sheet))

    def build_actions(self, frame):
        pass

    def build_dialog(self, sheet):
        pass

    def build_dialog_buttons(self, sheet):
        pass




class ModelScreen(Screen):

    def bind_update(self):
        return self.bind(self.do_update_form)

    def do_update_form(self):
        f = self.update()
        es = dict((k, v and str(v)) for (k,v) in f.errors.items())
        self.vars['form_errors'] = es
        self.vars['form_error_msg'] = self.form_error_msg
        for (k, v) in es.items():
            if v is not None:
                logging.log.debug('[form.error] %s: %s', k, v)

        pushback = f.get_pushback()
        if pushback:
            self.vars['form_pushback'] = pushback

    def update(self, args=None):
        if args is None:
            args = self.request.model_vars
            trigger = self.request.params.get('____trigger_field')
            args = trigger and {trigger: args.get(trigger)} or args
            only = trigger and [trigger]
        else:
            only = args.keys()

        f = self.get_form()
        f.validate(args, only)
        if type(self.model) is not type(f.model):
            # class changed
            self.add_callback('refresh_screen')
            self.errors = {}
        self.model = f.model
        return f

    def get_title(self):
        return str(self.model)

    __parent = util.undefined
    def get_parent(self):
        if self.parent:
            return self.parent
        if self.__parent is util.undefined:
            self.__parent = self._get_parent_from_history()
        return self.__parent

    def get_parents(self):
        parents = self.meta.get_parents(self.context, self.model)
        history = self.get_history()
        history = dict(reversed(history))
        for p in parents:
            v = self.context.get_view(p)
            p = v.get_path()
            if p:
                if p in history:
                    v.update(history[p])
                yield v

    def get_history(self):
        history = self.request.get_history()
        if self.referer:
            history = history + [self.referer]
        return history

    def _get_parent_from_history(self):
        parents = self.meta.get_parents(self.context, self.model)
        if not parents:
            return None
        history = self.get_history()
        if not history:
            return self.context.get_view(parents[0])
        for hpath, hargs in reversed(history):
            for p in parents:
                v = self.context.get_view(p)
                vpath = v and v.get_path()
                if not vpath:
                    continue
                if vpath == hpath:
                    v.update(hargs)
                    return v
        return v

    def get_priv(self, name):
        return self.meta.get_priv(name)




class Attachment(ModelScreen):
    registerable = True
    editable = True

    def get_args(self):
        return self.get_form().get_args()

    def render(self):
        rs = self.request.response
        rs.headers['Content-Type'] = self.model.attachment_type()
        rs.headers['Content-Disposition'] = 'attachment;filename="%s"' % self.model.attachment_filename()
        self.model.write_attachment_data(rs.body_file)




class IterScreen(ModelScreen):
    editable = True
    def do_update_form(self):
        ModelScreen.do_update_form(self)
        self.refresh()

    def build_filter_form(self, panel):
        for key, header, fields in self.get_form().groups:
            for field in fields:
                widgets.filter_field(panel, field=field)




class FormScreen(ModelScreen):
    editable = False

    def get_args(self):
        return None

    def right_body(self, body):
        widgets.model_form(body, groups=self.get_form().groups)
        if not self.editable:
            if getattr(self.model, '_meta', None):
                widgets.model_history(body)


    def bind_create_child(self, key, index):
        type = self.meta.attributes[key]
        if hasattr(type, 'meta'):
            priv = type.meta.get_priv('create')
        else:
            priv = True
        if self.check(priv):
            return self.bind(self.do_create_child, key, index)


    def bind_edit_child(self, obj, key, index):
        type = self.meta.attributes[key]
        if hasattr(type, 'meta'):
            priv = type.meta.get_priv('edit')
        else:
            priv = True
        if self.check(priv):
            return self.bind(self.do_edit_child, obj, key, index)


    def bind_delete_child(self, obj, key, index):
        type = self.meta.attributes[key]
        if hasattr(type, 'meta'):
            priv = type.meta.get_priv('delete')
        else:
            priv = True
        if self.check(priv):
            return self.bind(self.do_delete_child, obj, key, index)


    def do_edit_child(self, obj, key, index):
        self.update()
        Screen = self.meta.attributes[key].meta.edit_screen
        self.replace(Screen(self.context, obj, True, self, key, index))


    def do_create_child(self, key, index):
        self.update()
        obj = self.meta.construct_child(self.context, self.model, key, index)
        Screen = self.meta.attributes[key].meta.create_screen
        self.replace(Screen(self.context, obj, True, self, key, index))


    def do_delete_child(self, obj, key, index):
        self.meta.delete_child(self.context, self.model, key, index)
        self.update({key: getattr(self.model, key)})
        self.set_message('info', 'Removed %s' % obj)
        self.refresh()


    def store_child(self, obj, key, index):
        self.meta.store_child(self.context, self.model, obj, key, index)
        self.update({key: getattr(self.model, key)})




class EditableScreen(FormScreen):
    modal = True
    editable = True
    commit_label = 'Finish'
    def left_actions(self, panel):
        widgets.text(panel, text='UNSAVED', text_class='alert')
        widgets.toolbar_button(panel,
                               type='primary',
                               label=self.commit_label,
                               dirty_disables=True,
                               bind=self.bind(self.do_commit))
        widgets.toolbar_button(panel,
                               type='secondary',
                               label='Cancel',
                               bind=self.bind(self.do_cancel))

    def do_cancel(self):
        getmeta(self.model).purge_model(self.context, self.model)
        self.pop()

    def do_commit(self):
        obj = self.model
        self.update()
        if self.has_error:
            return self.refresh()
        self.commit_model()
        # instead,  call parent.commit_model()
        # and have it set the screen status ?
        screen = self.get_commit_screen()
        msg = self.get_commit_message()
        if msg:
            screen.set_message(*msg)
        self.replace(screen)

    def get_commit_message(self):
        pass

    def get_commit_screen(self):
        return self.get_parent()

    def commit_model(self):
        # self.parent.commit_model(self.model,...)
        with self.context:
            self.model = self.meta.store_model(self.context, self.model)



# concrete classes

class NavScreen(ModelScreen):
    registerable = True

    def right_body(self, body):
        self.build_links(widgets.link_list(body))

    def build_links(self, container):
        for label, url in self.get_links():
            url = self.request.absolute_url(url)
            widgets.link(container, url=url, label=label)

    def get_links(self):
        return [(v.get_title(), v.get_url())
                for v in self.model.get_links()]




class ListScreen(IterScreen):
    registerable = True

    def left_actions(self, panel):
        if self.check('create'):
            widgets.toolbar_button(panel, type='primary', label='New',
                                   bind=self.bind(self.do_create))
        self.build_filter_form(panel)

    def right_body(self, body):
        link_col = getattr(self.model, 'link_column', False)
        sm = self.model.submeta
        columns = (self.model.list_columns
                   # backward compat
                   or getattr(sm.model_class, '__list_columns__', [])
                   # fallback
                   or [sm.attributes.keys()[0]])
        widgets.model_list(body,
                           columns=columns,
                           link_column=link_col)

    def do_create(self):
        obj = self.model.submeta.construct_model(self.context)
        Screen = self.model.submeta.create_screen
        self.replace(Screen(self.context, obj, self.priv, self))




class ViewScreen(FormScreen):
    registerable = True
    editable = False


    def parent_links(self, panel):
        for p in self.get_parents():
            if p.check():
                widgets.parent_link(panel, url=p.get_url(), label=p.get_title())

    def left_actions(self, panel):
        self.parent_links(panel)
        if self.check('edit'):
            widgets.toolbar_button(panel,
                                   type='primary',
                                   label='Edit',
                                   bind=self.bind(self.do_edit, self.model))
        if self.check('delete'):
            widgets.toolbar_button(panel,
                                   type='primary',
                                   label='Delete',
                                   confirm='Delete %s?' % self.model,
                                   bind=self.bind(self.do_delete, self.model))

    def do_edit(self, obj=None):
        if obj is None:
            obj = self.model
        Screen = getmeta(obj).edit_screen
        self.replace(Screen(self.context, obj, self.priv, self))

    def do_delete(self, obj=None):
        if obj is None:
            obj = self.model
        with self.context:
            getmeta(obj).delete_model(self.context, obj)
        self.get_parent().set_message('ok', 'Deleted %s' % obj)
        self.pop()

    def do_clone(self, obj=None):
        if obj is None:
            obj = self.model
        new = getmeta(obj).construct_model(self.context)
        screen = CloneScreen(self.context, new, self.priv, self)
        form = CloneScreen(self.context, obj, self.priv, self).form
        screen.update(dict((k, form.get_value(k))
                             for k in form.fields))
        self.replace(screen)




class EditScreen(EditableScreen):
    def get_title(self):
        return '(editing)'

    def get_commit_message(self):
        return 'ok', 'Saved %s' % self.model




class CloneScreen(EditableScreen):
    def get_title(self):
        return '(cloning)'

    def get_commit_message(self):
        return 'ok', 'Cloned %s' % self.model




class CreateScreen(EditableScreen):
    def get_title(self):
        return '(new)'

    def get_commit_screen(self):
        return getmeta(self.model).get_view(self.context, self.model)

    def get_commit_message(self):
        return 'ok', 'Created %s' % self.model




class SubEditScreen(EditScreen):
    def left_actions(self, panel):
        widgets.text(panel, text='UNSAVED', text_class='alert')
        widgets.toolbar_button(panel,
                               type='primary',
                               label='Back',
                               dirty_disables=True,
                               bind=self.bind(self.do_commit))
    def get_title(self):
        return str(self.model)

    def commit_model(self):
        self.parent.store_child(self.model, self.child_key, self.child_index)

    def get_commit_message(self):
        return 'info', 'Updated %s (draft)' % self.model




class SubCreateScreen(SubEditScreen):
    def get_title(self):
        index = self.child_index
        suffix = index is not None and ' (%d)' % (index + 1)  or ''
        return str(self.model) + suffix

    def commit_model(self):
        self.parent.store_child(self.model, self.child_key, self.child_index)

    def get_commit_message(self):
        return 'info', 'Added %s (draft)' % self.model




class CommentScreen(EditableScreen):
    def get_title(self):
        return '(add comment)'

    def get_commit_message(self):
        return 'ok', 'Added comment'




class FormDialog(Dialog, EditableScreen):
    def build_dialog(self, sheet):
        widgets.model_form(sheet, groups=self.get_form().groups)




class CustomDialog(Dialog):
    def build_dialog(self, sheet):
        widgets.screen_widget(sheet, template_file=self.template_file)






